﻿using Base.FileCollect;
using Base.FileModel.BaseFile;
using Base.FileModel.DB;
using Base.FileModel.UserFile;
using Base.FileModel.UserFileDir;
using Base.FileRemoteModel.Pdf.Model;
using Base.FileService.Helper;
using Base.FileService.Interface;
using iText.Bouncycastle.Crypto;
using iText.Bouncycastle.X509;
using iText.Commons.Bouncycastle.Cert;
using iText.Commons.Bouncycastle.Crypto;
using iText.Forms.Form.Element;
using iText.IO.Image;
using iText.Kernel.Crypto;
using iText.Kernel.Geom;
using iText.Kernel.Pdf;
using iText.Layout.Properties;
using iText.Signatures;
using Org.BouncyCastle.Pkcs;
using WeDonekRpc.Helper;
using Path = System.IO.Path;
namespace Base.FileService.Pdf
{
    internal class WritePdfFileSignService : IWritePdfFileSignService
    {
        private class _PdfCert
        {
            public IPrivateKey pk;
            public IX509Certificate[] chain;
        }
        private readonly IUserFileCollect _UserFile;
        private readonly IFileConfig _Config;

        private readonly IUserFileDirCollect _FileDir;
        private readonly IFileCollect _File;
        private readonly IDirService _PhysicalDir;

        private readonly Dictionary<long, _PdfCert> _pfx = new Dictionary<long, _PdfCert>();
        private readonly Dictionary<long, string> _image = new Dictionary<long, string>();

        public WritePdfFileSignService ( IUserFileCollect userFile,
            IFileConfig config,
            IUserFileDirCollect fileDir,
            IFileCollect file,
            IDirService physicalDir )
        {
            this._UserFile = userFile;
            this._Config = config;
            this._FileDir = fileDir;
            this._File = file;
            this._PhysicalDir = physicalDir;
        }
        private WritePdfFileSign _PdfSign;
        public FilePdfResult WriteSign ( WritePdfFileSign obj )
        {
            this._PdfSign = obj;
            UserFileDirDto dir = this._FileDir.GetDir(obj.DirKey);
            List<long> fileId = new List<long>();
            fileId.Add(obj.PdfFileId);
            obj.Sign.ForEach(c =>
            {
                fileId.Add(c.SignFileId);
                fileId.Add(c.FileId);
            });
            UserFileBase[] files = this._UserFile.GetFiles(fileId.ToArray());
            UserFileBase pdf = files.Find(c => c.Id == obj.PdfFileId);
            obj.Sign.ForEach(c =>
            {
                if ( this._pfx.ContainsKey(c.SignFileId) == false )
                {
                    UserFileBase file = files.Find(a => a.Id == c.SignFileId);
                    string path = Path.Combine(file.DirPath, file.LocalPath);
                    Pkcs12Store store = new Pkcs12StoreBuilder().Build();
                    byte[] datas = File.ReadAllBytes(path);
                    store.Load(new MemoryStream(datas), c.CertPwd.ToArray());
                    string key = store.Aliases.FirstOrDefault(c => store.IsKeyEntry(c));
                    X509CertificateEntry entity = store.GetCertificate(key);
                    this._pfx.Add(c.SignFileId, new _PdfCert
                    {
                        pk = new PrivateKeyBC(store.GetKey(key).Key),
                        chain = new IX509Certificate[]
                        {
                            new X509CertificateBC(entity.Certificate)
                        }
                    });
                }
                if ( this._image.ContainsKey(c.FileId) == false )
                {
                    UserFileBase file = files.Find(a => a.Id == c.FileId);
                    this._image.Add(c.FileId, Path.Combine(file.DirPath, file.LocalPath));
                }
            });
            string path = Path.Combine(pdf.DirPath, pdf.LocalPath);
            DateTime now = DateTime.Now;
            MemoryStream pdfSteam = new MemoryStream(File.ReadAllBytes(path));
            obj.Sign.ForEach(c =>
            {
                pdfSteam.Position = 0;
                pdfSteam = this._WriteFile(c, pdfSteam);
            });
            FileDatum datum = this._SaveFile(obj, pdfSteam);
            return this._SaveUserFile(dir, datum);
        }
        private long _RegFile ( FileDatum add )
        {
            long fileId = this._File.FindFileId(add.FileMd5);
            if ( fileId == 0 )
            {
                return this._File.Add(add);
            }
            return fileId;
        }
        private FilePdfResult _SaveUserFile ( UserFileDirDto dir, FileDatum file )
        {
            long fileId = this._RegFile(file);
            UserFileAdd add = new UserFileAdd
            {
                UserId = this._PdfSign.EmpId,
                UserType = this._PdfSign.UserType,
                UserDirId = dir.Id,
                Power = dir.Power,
                PowerCode = dir.ReadPower,
                LinkBizPk = this._PdfSign.LinkBizPk,
                Tag = this._PdfSign.Tag,
                Extension = file.Extension,
                OperatePower = dir.UpPower,
                FileName = file.FileName,
                FileType = file.FileType,
                FileId = fileId
            };
            DBUserFileList userFile = this._UserFile.Add(add);
            return new FilePdfResult
            {
                FileId = userFile.Id,
                FileUri = userFile.GetFileUri(this._Config),
                FileName = file.FileName,
                Extension = file.Extension
            };
        }
        private FileDatum _SaveFile ( WritePdfFileSign obj, MemoryStream stream )
        {
            stream.Position = 0;
            long fileSize = stream.Length;
            IDirState dir = this._PhysicalDir.GetDir(ref fileSize);
            string md5 = Tools.GetMD5(stream);
            string ext = ".pdf";
            FileDatum datum = new FileDatum
            {
                Extension = ext,
                FileSize = stream.Length,
                FileMd5 = md5,
                FileName = obj.PdfFileId + ext,
                DirId = dir.Id,
                FileType = FileRemoteModel.FileType.doc,
                DirPath = dir.DirPath,
                LocalPath = Path.Combine("file_pdf", "Zone_" + md5.GetZIndex(), md5 + ext)
            };
            string path = dir.GetFullPath(datum.LocalPath);
            FileInfo file = new FileInfo(path);
            if ( !file.Directory.Exists )
            {
                file.Directory.Create();
            }
            using ( FileStream fs = file.Open(FileMode.Create, FileAccess.ReadWrite, FileShare.Delete) )
            {
                byte[] buffer = stream.ToArray();
                fs.Write(buffer, 0, buffer.Length);
                fs.Flush();
            }
            return datum;
        }
        private MemoryStream _WriteFile ( WritePdfSign sign, MemoryStream pdfStream )
        {
            _PdfCert cert = this._pfx[sign.SignFileId];
            string img = this._image[sign.FileId];
            TempMemoryStream outStream = new TempMemoryStream();
            using ( PdfOutputStream stream = new PdfOutputStream(outStream) )
            {
                using ( PdfReader reader = new PdfReader(pdfStream) )
                {
                    DateTime now = DateTime.Now;
                    PdfSigner signer = new PdfSigner(reader, stream, new StampingProperties());
                    SignerProperties pro = signer.GetSignerProperties();
                    _ = pro.SetLocation(sign.Location ?? string.Empty);
                    _ = pro.SetReason(sign.Reason ?? string.Empty);
                    _ = pro.SetClaimedSignDate(now);
                    SignatureFieldAppearance app = new SignatureFieldAppearance(sign.SignFileId.ToString());
                    _ = app.SetPageNumber(sign.Index);
                    ImageData data = ImageDataFactory.Create(img);
                    _ = app.SetContent(data);
                    app.SetProperty(Property.HEIGHT, UnitValue.CreatePointValue(sign.Height));
                    app.SetProperty(Property.WIDTH, UnitValue.CreatePointValue(sign.Width));
                    _ = pro.SetSignatureAppearance(app);
                    _ = pro.SetPageNumber(sign.Index);
                    using ( PdfDocument doc = signer.GetDocument() )
                    {
                        PdfPage page = doc.GetPage(sign.Index);
                        Rectangle rectangle = page.GetPageSizeWithRotation();
                        double ratio = Math.Round(rectangle.GetWidth() * 100 / this._PdfSign.Width, 1);
                        sign.X = (int)Math.Round(sign.X * ratio / 100.0, 0);
                        sign.Width = (int)Math.Round(sign.Width * ratio / 100.0, 0);
                        sign.Height = (int)Math.Round(sign.Height * ratio / 100.0, 0);
                        sign.Y = (int)( rectangle.GetHeight() - sign.Height - (int)Math.Round(sign.Y * ratio / 100.0, 0) );
                        _ = pro.SetPageRect(new Rectangle(sign.X, sign.Y, sign.Width, sign.Height));
                        IExternalSignature pks = sign.SignType == WritePdfSignType.RSA ? new PrivateKeySignature(cert.pk, DigestAlgorithms.SHA256) : new Sm2SignExtend(cert.pk);
                        signer.SignDetached(new BouncyCastleDigest(), pks, cert.chain, null, null, null, 0, PdfSigner.CryptoStandard.CADES);
                        return new MemoryStream(outStream.ToBytes());
                    }
                }
            }
        }
    }
}
